﻿/**

 * Copyright (c) 2015-2017, FastDev 刘强 (fastdev@163.com).

 *

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *      http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

using Mono.Cecil;
using Mono.Cecil.Cil;
using OF.ILModifier.Common;
using OF.ILModifier.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using OF.ILModifier.Extension;

namespace OF.ILModifier.Handler
{
    public class MethodReplaceHandler
    {

        private AssemblyHelper asmHelper;
        public MethodReplaceHandler(AssemblyHelper asmHelper)
        {
            this.asmHelper = asmHelper;
        }

        private void CheckCommonDefinition(TypeDefinition targetType,
            MethodDefinition callMethod,
            MethodInfo replaceMethod,
            Action<ParamContainer, Exception> onException,
            out ReplaceCommonCheckResult checkResult)
        {
            if (callMethod == null)
            {
                throw new NotSupportedException("The method to be invoked can't be null!");
            }


            if (!replaceMethod.IsStatic)
            {
                throw new NotSupportedException("The method to replace must be static!");
            }

            if (onException == null)
            {
                throw new NotSupportedException("The method to handle exception can't be null");
            }

            if (!onException.Method.IsStatic)
            {
                throw new NotSupportedException("The method to handle exception must be static");
            }
            Action<string, MethodReference> checkInvokeAction = (invokeName, method) =>
            {
                if (method != null)
                {
                    if (method.Parameters.Count != 1)
                    {
                        throw new NotSupportedException(invokeName + " can only have one param with type [" + AssemblyHelper.ParamContainerTypeName + "]");
                    }
                    else
                    {
                        if (!method.Parameters[0].ParameterType.GetFullName().Equals(asmHelper.ParamContainerFullTypeName))
                        {
                            throw new NotSupportedException(invokeName + " can only have one param with type [" + AssemblyHelper.ParamContainerTypeName + "]");
                        }
                    }
                }
            };
            Type paramContainerType = typeof(ParamContainer);
            var paramContainerRefer = asmHelper.ImportReference(paramContainerType);
            MethodReference replaceMethodRefer = asmHelper.ImportReference(replaceMethod);
            checkInvokeAction("replaceMethod", replaceMethodRefer);
            List<TypeReference> paramTypeList = new List<TypeReference>();
            int paramsCount = callMethod.Parameters.Count;
            if (callMethod is MethodDefinition
                && !((MethodDefinition)callMethod).IsStatic)
            {
                paramsCount = paramsCount + 1;
                paramTypeList.Add(targetType);
            }
            paramTypeList.AddRange(callMethod.Parameters.Select(prm => prm.ParameterType).ToList());
            string errorMessage = "replaceMethod should have ParamContainer as return value";
            
            if (!replaceMethodRefer.HasReturnValue())
            {
                throw new NotSupportedException(errorMessage);
            }
            if (replaceMethodRefer.ReturnType.GetFullName() != paramContainerRefer.GetFullName())
            {
                throw new NotSupportedException(errorMessage);
            }
          
            checkResult.paramsCount = paramsCount;
            checkResult.paramTypeList = paramTypeList;
            checkResult.replaceMethodRefer = replaceMethodRefer;
        }

        public struct ReplaceCommonCheckResult
        {
            public int paramsCount;
            public List<TypeReference> paramTypeList;
            public MethodReference replaceMethodRefer;
        }

        public void ReplaceCommonDefinition(TypeDefinition targetType,
            MethodDefinition callMethod,
            Func<ParamContainer, ParamContainer> replaceFunc,
            Action<ParamContainer, Exception> onException)
        {
            if (replaceFunc == null)
            {
                throw new NotSupportedException("The method to replace can't be null!");
            }
            ReplaceCommonDefinitionCore(targetType,
                callMethod,
                replaceFunc.Method,
                onException);
        }

        private void ReplaceCommonDefinitionCore(TypeDefinition targetType,
            MethodDefinition callMethod,
            MethodInfo replaceMethod,
            Action<ParamContainer, Exception> onException)
        {
            ReplaceCommonCheckResult checkResult;
            CheckCommonDefinition(targetType,
                callMethod,
                replaceMethod,
                onException,
                out checkResult);
            List<Instruction> result = new List<Instruction>((checkResult.paramsCount + 1) * 2 + 1);
            AddCommonReplaceCall(targetType, callMethod, checkResult.paramTypeList, result, checkResult.replaceMethodRefer, onException);
        }

        private void AddCommonReplaceCall(TypeDefinition targetType, MethodDefinition callMethod, List<TypeReference> paramTypeList,
            List<Instruction> result, MethodReference replaceMethod, Action<ParamContainer, Exception> onException)
        {
            if (replaceMethod == null)
            {
                return;
            }
            ILProcessor pWorker = callMethod.Body.GetILProcessor();
            MethodDefinition movedMethod = MethodHelper.MoveMethod(targetType, callMethod);
            callMethod.Body.Variables.Clear();
            callMethod.Body.Variables.Add(new VariableDefinition(asmHelper.ImportReference(typeof(ParamContainer))));
            callMethod.Body.Variables.Add(new VariableDefinition(asmHelper.ImportReference(typeof(bool))));
            callMethod.Body.Variables.Add(new VariableDefinition(asmHelper.ImportReference(typeof(Exception))));
            if (callMethod.HasReturnValue())
            {
                callMethod.Body.Variables.Add(new VariableDefinition(callMethod.ReturnType));
            }
            Instruction insCall;
            insCall = pWorker.Create(OpCodes.Newobj, asmHelper.ParamContainerConstructor);
            result.Add(insCall);
            result.Add(pWorker.Create(OpCodes.Stloc_0));
            result.Add(pWorker.Create(OpCodes.Ldloc_0));
            MethodHelper.LoadParams(result, pWorker, paramTypeList, (addParamIns, paramType) => {
                result.Add(addParamIns);
                MethodReference convertParam = MethodHelper.MakeGenericMethod(asmHelper.ParamContainerAddMethod, new TypeReference[] { paramType });
                result.Add(pWorker.Create(OpCodes.Call, convertParam));
            });
            /*
            result.Add(pWorker.Create(OpCodes.Pop));
            result.Add(pWorker.Create(OpCodes.Ldloc_0));
            */
            result.Add(pWorker.Create(OpCodes.Call, replaceMethod));

            MethodInfo hasResultMethod = typeof(ParamContainer).GetMethod("HasResult");
            result.Add(pWorker.Create(OpCodes.Call, asmHelper.ImportReference(hasResultMethod)));
            /*
            result.Add(pWorker.Create(OpCodes.Stloc_1));
            result.Add(pWorker.Create(OpCodes.Ldloc_1));
            */
            Instruction noResultJump = pWorker.Create(OpCodes.Nop);
            Instruction ldResultValue = null;

            result.Add(pWorker.Create(OpCodes.Brfalse_S, noResultJump));
            MethodReference getResultMethod = asmHelper.ImportReference(typeof(ParamContainer).GetMethod("GetResult"));
            if (callMethod.HasReturnValue())
            {
                ldResultValue = pWorker.Create(OpCodes.Ldloc_3);

                result.Add(pWorker.Create(OpCodes.Ldloc_0));
                var genMethod = MethodHelper.MakeGenericMethod(getResultMethod, callMethod.ReturnType);
                result.Add(pWorker.Create(OpCodes.Callvirt, genMethod));
                result.Add(pWorker.Create(OpCodes.Stloc_3));
            }
            else
            {
                ldResultValue = pWorker.Create(OpCodes.Nop);
            }
            result.Add(pWorker.Create(OpCodes.Br_S, ldResultValue));
            result.Add(noResultJump);

            MethodHelper.LoadParams(result, pWorker, paramTypeList, (addParamIns, paramType) => {
                result.Add(addParamIns);
            });
            result.Add(pWorker.Create(OpCodes.Call, MethodHelper.GetCallMethod(movedMethod, callMethod)));
            
            if (callMethod.HasReturnValue())
            {
                result.Add(pWorker.Create(OpCodes.Stloc_3));
            }
            result.Add(pWorker.Create(OpCodes.Leave_S, ldResultValue));
            Instruction catchStart = pWorker.Create(OpCodes.Stloc_2);
            result.Add(catchStart);
            result.Add(pWorker.Create(OpCodes.Ldloc_0));
            result.Add(pWorker.Create(OpCodes.Ldloc_2));
            MethodReference exceptionMethod = asmHelper.ImportReference(onException.Method);
            result.Add(pWorker.Create(OpCodes.Call, exceptionMethod));
            result.Add(pWorker.Create(OpCodes.Rethrow));
            result.Add(ldResultValue);
            result.Add(pWorker.Create(OpCodes.Ret));
            

            foreach (var handler in callMethod.Body.ExceptionHandlers.ToList())
            {
                callMethod.Body.ExceptionHandlers.Remove(handler);
            }

            callMethod.Body.ExceptionHandlers.Add(new ExceptionHandler(ExceptionHandlerType.Catch)
            {
                CatchType = asmHelper.ImportReference(typeof(System.Exception)),
                FilterStart = null,
                HandlerType = ExceptionHandlerType.Catch,
                TryStart = noResultJump,
                TryEnd = catchStart,
                HandlerStart = catchStart,
                HandlerEnd = ldResultValue
            });
            var insList = result.ToArray();
            ILProcessor process = callMethod.Body.GetILProcessor();
            foreach (var ins in insList)
            {
                process.Append(ins);
            }
        }
    }
}
